You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
469 lines
13 KiB
469 lines
13 KiB
import {
|
|
render,
|
|
screen,
|
|
fireEvent,
|
|
waitFor,
|
|
cleanup,
|
|
} from "@testing-library/react";
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import QuestionDetailClient from "./question-detail-client";
|
|
import { useCattellQuestionsQuery } from "@/hooks/marriage/use-cattell";
|
|
import { useGlasserQuestionsQuery } from "@/hooks/marriage/use-glasser";
|
|
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
|
|
import {
|
|
useFormOverviewQuery,
|
|
useFormSectionQuery,
|
|
} from "@/hooks/marriage/use-form-schema";
|
|
import {
|
|
convertOverviewToFrontendItems,
|
|
mapBackendSectionToFrontend,
|
|
} from "@/lib/schema-adapter";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
|
|
vi.mock("next/navigation", () => ({
|
|
useRouter: vi.fn(() => ({ replace: vi.fn(), push: vi.fn() })),
|
|
}));
|
|
|
|
vi.mock("@/translations/provider", () => ({
|
|
useI18n: vi.fn(() => ({
|
|
locale: "en",
|
|
dictionary: new Proxy({}, { get: (_, key) => key }),
|
|
})),
|
|
}));
|
|
|
|
vi.mock("@/hooks/marriage/use-cattell", () => ({
|
|
useCattellQuestionsQuery: vi.fn(),
|
|
useSubmitCattellAssessmentMutation: vi.fn(() => ({ mutateAsync: vi.fn() })),
|
|
}));
|
|
|
|
vi.mock("@/hooks/marriage/use-glasser", () => ({
|
|
useGlasserQuestionsQuery: vi.fn(),
|
|
useSubmitGlasserAssessmentMutation: vi.fn(() => ({ mutateAsync: vi.fn() })),
|
|
}));
|
|
|
|
vi.mock("@/hooks/marriage/use-profile-main", () => ({
|
|
useMarriageProfileQuery: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/hooks/marriage/use-form-schema", () => ({
|
|
useFormOverviewQuery: vi.fn(),
|
|
useFormSectionQuery: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/hooks/marriage/use-habcoin-payment", () => ({
|
|
useHabcoinPaymentMutation: vi.fn(() => ({ mutateAsync: vi.fn() })),
|
|
}));
|
|
|
|
vi.mock("@/lib/schema-adapter", () => ({
|
|
convertOverviewToFrontendItems: vi.fn(),
|
|
mapBackendSectionToFrontend: vi.fn(),
|
|
}));
|
|
|
|
describe("QuestionDetailClient Validation", () => {
|
|
const mockCattellRefetch = vi.fn();
|
|
const mockGlasserRefetch = vi.fn();
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
|
|
(useMarriageProfileQuery as any).mockReturnValue({
|
|
data: { age: 30, gender: "male" },
|
|
isLoading: false,
|
|
});
|
|
|
|
(useFormOverviewQuery as any).mockReturnValue({
|
|
data: {},
|
|
isLoading: false,
|
|
});
|
|
(useFormSectionQuery as any).mockReturnValue({
|
|
data: undefined,
|
|
isLoading: false,
|
|
});
|
|
});
|
|
|
|
const setupTest = (slug: string, cattellData: any, glasserData: any) => {
|
|
// Mock schema adapter to return an item for the requested slug
|
|
(convertOverviewToFrontendItems as any).mockReturnValue([
|
|
{
|
|
slug: slug,
|
|
title: "Test",
|
|
questions: [],
|
|
},
|
|
]);
|
|
|
|
(useCattellQuestionsQuery as any).mockReturnValue({
|
|
data: cattellData,
|
|
isLoading: false,
|
|
isError: false,
|
|
refetch: mockCattellRefetch,
|
|
});
|
|
|
|
(useGlasserQuestionsQuery as any).mockReturnValue({
|
|
data: glasserData,
|
|
isLoading: false,
|
|
isError: false,
|
|
refetch: mockGlasserRefetch,
|
|
});
|
|
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
retry: false,
|
|
},
|
|
},
|
|
});
|
|
|
|
render(
|
|
<QueryClientProvider client={queryClient}>
|
|
<QuestionDetailClient
|
|
closeLabel="Close"
|
|
continueLabel="Continue"
|
|
description="Desc"
|
|
informationLabel="Info"
|
|
itemSlug={slug}
|
|
questionsListHref="/questions-list"
|
|
title="Test"
|
|
/>
|
|
</QueryClientProvider>,
|
|
);
|
|
|
|
// Intro screen might render first; if Start is available, click it to mount questions flow
|
|
const startButton = screen.queryByText("Start");
|
|
if (startButton) {
|
|
fireEvent.click(startButton);
|
|
}
|
|
};
|
|
|
|
it("should render correctly when Cattell API data is completely valid", () => {
|
|
setupTest(
|
|
"personality_test",
|
|
{
|
|
questions: [
|
|
{
|
|
question_number: 1,
|
|
text: "Valid Question Cattell",
|
|
options: [
|
|
{ id: "opt_a", label: "Opt1", value: "A" },
|
|
{ id: "opt_b", label: "Opt2", value: "B" },
|
|
{ id: "opt_c", label: "Opt3", value: "C" },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
null,
|
|
);
|
|
|
|
// Retry UI should NOT be present
|
|
expect(screen.queryByText("Retry")).toBeNull();
|
|
// Question text should be visible
|
|
expect(screen.getByText("Valid Question Cattell")).toBeDefined();
|
|
});
|
|
|
|
it("should render Retry UI when Cattell API data is empty", () => {
|
|
setupTest("personality_test", { questions: [] }, null);
|
|
|
|
expect(
|
|
screen.getAllByText("No questions found for this test."),
|
|
).toBeDefined();
|
|
expect(screen.getAllByText("Retry")).toBeDefined();
|
|
});
|
|
|
|
it("should render Retry UI when Cattell options are invalid (schema failure) and trigger refetch on Retry", async () => {
|
|
setupTest(
|
|
"personality_test",
|
|
{
|
|
questions: [
|
|
{
|
|
question_number: 1,
|
|
text: "Invalid Question",
|
|
options: [{ label: "Opt1", value: "A" }], // Invalid schema
|
|
},
|
|
],
|
|
},
|
|
null,
|
|
);
|
|
|
|
expect(
|
|
screen.getAllByText("No questions found for this test."),
|
|
).toBeDefined();
|
|
const retryBtn = screen.getAllByText("Retry")[0];
|
|
|
|
fireEvent.click(retryBtn);
|
|
await waitFor(() => {
|
|
expect(mockCattellRefetch).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
it("should render correctly when Glasser API data is completely valid", () => {
|
|
setupTest("glasser_5_needs_test", null, {
|
|
questions: [
|
|
{
|
|
question_number: 1,
|
|
text: "Valid Question Glasser",
|
|
factor_code: "SUR",
|
|
options: [
|
|
{ id: "o1", label: "O1", value: 1 },
|
|
{ id: "o2", label: "O2", value: 2 },
|
|
{ id: "o3", label: "O3", value: 3 },
|
|
{ id: "o4", label: "O4", value: 4 },
|
|
{ id: "o5", label: "O5", value: 5 },
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(screen.queryByText("Retry")).toBeNull();
|
|
expect(screen.getByText("Valid Question Glasser")).toBeDefined();
|
|
});
|
|
|
|
it("should render Retry UI when Glasser options are invalid (schema failure) and trigger refetch on Retry", async () => {
|
|
setupTest("glasser_5_needs_test", null, {
|
|
questions: [
|
|
{
|
|
question_number: 1,
|
|
text: "Invalid Question Glasser",
|
|
factor_code: "SUR",
|
|
options: [
|
|
{ label: "O1", value: 1 },
|
|
{ label: "O2", value: 2 },
|
|
{ label: "O3", value: 3 },
|
|
{ label: "O4", value: 4 },
|
|
],
|
|
},
|
|
],
|
|
});
|
|
expect(
|
|
screen.getAllByText("No questions found for this test."),
|
|
).toBeDefined();
|
|
const retryBtn = screen.getAllByText("Retry")[0];
|
|
|
|
fireEvent.click(retryBtn);
|
|
await waitFor(() => {
|
|
expect(mockGlasserRefetch).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
it("should render profile questions using ID-based data flow", () => {
|
|
const profileItem = {
|
|
slug: "profile_test",
|
|
title: "Profile Form",
|
|
questions: [
|
|
{
|
|
id: "q_123",
|
|
title: "Dynamic ID Question",
|
|
type: "text",
|
|
order: 1,
|
|
required: true,
|
|
isVisible: true,
|
|
private: false,
|
|
description: "",
|
|
tooltip: "",
|
|
extras: {},
|
|
options: [],
|
|
},
|
|
{
|
|
id: "q_456",
|
|
title: "Another ID Question",
|
|
type: "radio",
|
|
order: 2,
|
|
required: false,
|
|
isVisible: true,
|
|
private: false,
|
|
description: "",
|
|
tooltip: "",
|
|
extras: {},
|
|
options: [
|
|
{ id: "opt_1", label: "Yes", value: "yes", order: 1 },
|
|
{ id: "opt_2", label: "No", value: "no", order: 2 },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
(convertOverviewToFrontendItems as any).mockReturnValue([
|
|
{
|
|
slug: "profile_test",
|
|
title: "Profile Form",
|
|
questions: [],
|
|
},
|
|
]);
|
|
(useFormSectionQuery as any).mockReturnValue({
|
|
data: {
|
|
section: { cards: [] },
|
|
answers: {},
|
|
section_progress: { completion_percent: 0 },
|
|
},
|
|
isLoading: false,
|
|
});
|
|
(mapBackendSectionToFrontend as any).mockReturnValue(profileItem);
|
|
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: { queries: { retry: false } },
|
|
});
|
|
|
|
render(
|
|
<QueryClientProvider client={queryClient}>
|
|
<QuestionDetailClient
|
|
closeLabel="Close"
|
|
continueLabel="Continue"
|
|
description="Desc"
|
|
informationLabel="Info"
|
|
itemSlug="profile_test"
|
|
questionsListHref="/questions-list"
|
|
title="Profile Test"
|
|
/>
|
|
</QueryClientProvider>,
|
|
);
|
|
|
|
// Profile questions render directly, no start button
|
|
expect(screen.getByText(/Dynamic ID/)).toBeDefined();
|
|
});
|
|
|
|
it("renders a stable profile loading shell while section data is cold", () => {
|
|
(convertOverviewToFrontendItems as any).mockReturnValue([
|
|
{
|
|
slug: "profile_test",
|
|
title: "Overview Profile Title",
|
|
questions: [],
|
|
},
|
|
]);
|
|
(useFormSectionQuery as any).mockReturnValue({
|
|
data: undefined,
|
|
isLoading: true,
|
|
});
|
|
(useCattellQuestionsQuery as any).mockReturnValue({
|
|
data: undefined,
|
|
isLoading: false,
|
|
});
|
|
(useGlasserQuestionsQuery as any).mockReturnValue({
|
|
data: undefined,
|
|
isLoading: false,
|
|
});
|
|
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: { queries: { retry: false } },
|
|
});
|
|
|
|
render(
|
|
<QueryClientProvider client={queryClient}>
|
|
<QuestionDetailClient
|
|
closeLabel="Close"
|
|
continueLabel="Continue"
|
|
description="Desc"
|
|
informationLabel="Info"
|
|
itemSlug="profile_test"
|
|
questionsListHref="/questions-list"
|
|
title="Route Fallback Title"
|
|
/>
|
|
</QueryClientProvider>,
|
|
);
|
|
|
|
expect(screen.getByText("Overview Profile Title")).toBeDefined();
|
|
expect(screen.getByRole("status")).toBeDefined();
|
|
expect(screen.getByRole("button", { name: "Close" })).toBeDefined();
|
|
expect(screen.getByRole("button", { name: "Info" })).toBeDefined();
|
|
expect(screen.getByRole("button", { name: "Continue" })).toBeDisabled();
|
|
expect(document.querySelector(".shimmer-bg")).toBeNull();
|
|
});
|
|
|
|
it("renders cached section questions while the overview refreshes", () => {
|
|
const cachedItem = {
|
|
slug: "profile_test",
|
|
title: "Cached Profile Form",
|
|
questions: [
|
|
{
|
|
id: "cached_question",
|
|
title: "Cached question",
|
|
type: "text",
|
|
order: 1,
|
|
required: true,
|
|
isVisible: true,
|
|
private: false,
|
|
description: "",
|
|
tooltip: "",
|
|
extras: { placeHolder: "", range: [0, 0], options: [] },
|
|
options: [],
|
|
},
|
|
],
|
|
};
|
|
(convertOverviewToFrontendItems as any).mockReturnValue([]);
|
|
(useFormOverviewQuery as any).mockReturnValue({
|
|
data: undefined,
|
|
isLoading: true,
|
|
});
|
|
(useFormSectionQuery as any).mockReturnValue({
|
|
data: {
|
|
section: { cards: [] },
|
|
answers: {},
|
|
section_progress: { completion_percent: 0 },
|
|
},
|
|
isLoading: false,
|
|
});
|
|
(mapBackendSectionToFrontend as any).mockReturnValue(cachedItem);
|
|
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: { queries: { retry: false } },
|
|
});
|
|
render(
|
|
<QueryClientProvider client={queryClient}>
|
|
<QuestionDetailClient
|
|
closeLabel="Close"
|
|
continueLabel="Continue"
|
|
description="Desc"
|
|
informationLabel="Info"
|
|
itemSlug="profile_test"
|
|
questionsListHref="/questions-list"
|
|
title="Route Fallback Title"
|
|
/>
|
|
</QueryClientProvider>,
|
|
);
|
|
|
|
expect(screen.getByRole("textbox")).toBeDefined();
|
|
expect(screen.queryByRole("status")).toBeNull();
|
|
});
|
|
|
|
it("uses the route title when the overview is also cold", () => {
|
|
(convertOverviewToFrontendItems as any).mockReturnValue([]);
|
|
(useFormOverviewQuery as any).mockReturnValue({
|
|
data: undefined,
|
|
isLoading: true,
|
|
});
|
|
(useFormSectionQuery as any).mockReturnValue({
|
|
data: undefined,
|
|
isLoading: true,
|
|
});
|
|
(useCattellQuestionsQuery as any).mockReturnValue({
|
|
data: undefined,
|
|
isLoading: false,
|
|
});
|
|
(useGlasserQuestionsQuery as any).mockReturnValue({
|
|
data: undefined,
|
|
isLoading: false,
|
|
});
|
|
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: { queries: { retry: false } },
|
|
});
|
|
|
|
render(
|
|
<QueryClientProvider client={queryClient}>
|
|
<QuestionDetailClient
|
|
closeLabel="Close"
|
|
continueLabel="Continue"
|
|
description="Desc"
|
|
informationLabel="Info"
|
|
itemSlug="profile_test"
|
|
questionsListHref="/questions-list"
|
|
title="Route Fallback Title"
|
|
/>
|
|
</QueryClientProvider>,
|
|
);
|
|
|
|
expect(screen.getByText("Route Fallback Title")).toBeDefined();
|
|
expect(screen.getByRole("status")).toBeDefined();
|
|
expect(document.querySelector(".shimmer-bg")).toBeNull();
|
|
});
|
|
});
|